Skip to content

feat(nucleus): Add NucleusClient.merge_model_runs() - #474

Merged
luke-e-schaefer merged 6 commits into
masterfrom
lukeschaefer/merge-model-runs
Aug 21, 2026
Merged

feat(nucleus): Add NucleusClient.merge_model_runs()#474
luke-e-schaefer merged 6 commits into
masterfrom
lukeschaefer/merge-model-runs

Conversation

@luke-e-schaefer

@luke-e-schaefer luke-e-schaefer commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Adds NucleusClient.merge_model_runs(model_run_ids, name=None, *, metadata=None), wrapping POST /v1/nucleus/modelRun/merge.

Requires scaleapi PR https://github.com/scaleapi/scaleapi/pull/156919, which adds the endpoint.

Why

A benchmark evaluation names a single model run, and a benchmark's items may span several datasets. A model whose predictions were uploaded as separate runs — one per dataset, or one per inference batch — has no single run covering the benchmark, so every uncovered item scores as a false negative. Merge first, wait for the copy to finish, then pass the new run to create_benchmark_evaluation_v2().

result = client.merge_model_runs(["run_abc", "run_def", "run_ghi"])
result["job"].sleep_until_complete()
evaluation = client.create_benchmark_evaluation_v2(
    benchmark_id, result["model_run_id"]
)

Semantics

Asynchronous. The endpoint returns 202 immediately with {"model_run_id", "dataset_ids", "job"}. The new run exists and is authorized right away, but its predictions are copied by a background job, so the run is empty until the job completes. Wait on result["job"] (an AsyncJob) before evaluating — otherwise the evaluation scores the not-yet-copied items as false negatives, the very failure this feature is meant to fix.

All source runs must belong to the same model; merging across models is rejected server-side (a run's model is its provenance, read by eval, leaderboards and the model page).

Full union — predictions are copied, never deduplicated, and the source runs are left untouched. If two source runs predict on the same item with the same annotation_id, the colliding id is rewritten rather than dropped. The copy's counts (predictions_copied, predictions_ignored, annotation_ids_rewritten) and any errors are reported on the job, not in the immediate response.

Verification

The test suite requires live API keys (conftest.py hard-asserts on NUCLEUS_PYTEST_API_KEY), so it could not be run locally, and there is no local backend to exercise the round trip against. Verified offline against the endpoint's Joi schema and handler in scaleapi #156919: the method builds the exact payload the server accepts (model_run_ids, optional name / metadata — no model_id, which the schema rejects and which the same-model constraint makes meaningless), posts to modelRun/merge, wraps the 202 response's job_id in an AsyncJob, and raises on fewer than two distinct run ids before making a request.

Note on the version bump

Bumped to 0.21.0. PR #473 also bumps pyproject.toml / CHANGELOG.md; whichever lands second will need a trivial rebase on those two files.

🤖 Generated with Claude Code

Greptile Summary

Adds NucleusClient.merge_model_runs() for asynchronously combining predictions from multiple same-model runs into a new run.

  • Deduplicates source run IDs while preserving their order and validates that at least two distinct runs were supplied.
  • Sends optional name and metadata fields to the merge endpoint.
  • Returns the merged model-run ID, covered dataset IDs, and an AsyncJob handle for monitoring the background copy.
  • Adds the new request-key constant and release notes.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
nucleus/init.py Adds the public merge wrapper, request validation and serialization, and asynchronous-job response handling.
nucleus/constants.py Adds the canonical model_run_ids wire-key constant used by the merge request.
CHANGELOG.md Documents the merge operation, asynchronous copy requirement, and full-union semantics.

Sequence Diagram

sequenceDiagram
    participant U as SDK caller
    participant C as NucleusClient
    participant A as Nucleus API
    participant J as AsyncJob
    participant E as Evaluation V2
    U->>C: merge_model_runs(run IDs, name, metadata)
    C->>C: Deduplicate and validate run IDs
    C->>A: POST modelRun/merge
    A-->>C: model_run_id, dataset_ids, job metadata
    C-->>U: model_run_id, dataset_ids, AsyncJob
    U->>J: sleep_until_complete()
    J->>A: Poll job status
    A-->>J: Completed
    U->>E: create_benchmark_evaluation_v2(benchmark, merged run)
Loading

Reviews (6): Last reviewed commit: "Merge branch 'master' into lukeschaefer/..." | Re-trigger Greptile

Context used (4)

A benchmark evaluation names a single model run, and a benchmark's items
may span several datasets. A model whose predictions were uploaded as
separate runs — one per dataset, or one per inference batch — therefore
had no single run covering the benchmark, and every uncovered item
scored as a false negative. Merging the runs produces one run that does
cover it, which can then be passed to create_benchmark_evaluation_v2().

The merge is a full union: predictions are copied, never deduplicated.
Colliding annotation_ids are rewritten rather than dropped, and the
response reports predictions_copied, predictions_ignored and
annotation_ids_rewritten so nothing is lost silently.

Wraps POST /v1/nucleus/modelRun/merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@luke-e-schaefer luke-e-schaefer changed the title [DE-XXXX] Add NucleusClient.merge_model_runs() feat(nucleus): Add NucleusClient.merge_model_runs() Aug 17, 2026
@luke-e-schaefer
luke-e-schaefer marked this pull request as ready for review August 17, 2026 17:05
@luke-e-schaefer luke-e-schaefer self-assigned this Aug 17, 2026
@luke-e-schaefer
luke-e-schaefer requested a review from a team August 17, 2026 17:05
luke-e-schaefer and others added 2 commits August 18, 2026 18:01
Drop the unsupported model_id parameter (the backend Joi schema rejects
unknown keys and forbids cross-model merges), handle the async 202
response by returning an AsyncJob so callers wait before evaluating, make
name optional to match the server default, and correct the docstring and
CHANGELOG to describe the real return shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@edwinpav

Copy link
Copy Markdown
Contributor

👀

@edwinpav edwinpav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly nits, one main comment here: #474 (comment)

Comment thread nucleus/__init__.py Outdated
Comment thread nucleus/__init__.py Outdated
Comment thread nucleus/__init__.py Outdated
Comment thread nucleus/__init__.py Outdated
Comment thread nucleus/__init__.py Outdated
@luke-e-schaefer
luke-e-schaefer merged commit a01c830 into master Aug 21, 2026
9 checks passed
@luke-e-schaefer
luke-e-schaefer deleted the lukeschaefer/merge-model-runs branch August 21, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants